package org.springframework.security.web.authentication;

import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.log.LogMessage;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.*;
import org.springframework.security.web.util.RedirectUrlBuilder;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import java.io.IOException;

/**
 * @author Dillon
 * @date 2024/6/30
 * @slogan 致敬大师 致敬未来的你
 * @desc springSecurity 认证引导接口
 * 基于httpUrl的认证处理器
 */
public class LoginUrlAuthenticationEntryPoint implements AuthenticationEntryPoint, InitializingBean {

	/**
	 * 日志对象
	 */
	private static final Log logger = LogFactory.getLog(LoginUrlAuthenticationEntryPoint.class);

	/**
	 * http 端口辅助类 常用于 将http https 端口进行映射 与PortResolver 一起使用
	 */
	private PortMapper portMapper = new PortMapperImpl();

	/**
	 * 端口解析器
	 */
	private PortResolver portResolver = new PortResolverImpl();

	/**
	 * login 表单登录路径
	 */
	private final String loginFormUrl;

	/**
	 * 是否强制使用https
	 */
	private boolean forceHttps = false;

	/**
	 * 是否使用服务器内部处理请求
	 */
	private boolean useForward = false;

	/**
	 * 重定向策略
	 */
	private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

	/**
	 * 构造函数
	 *
	 * @param loginFormUrl 表单登录路径
	 */
	public LoginUrlAuthenticationEntryPoint(String loginFormUrl) {
		Assert.notNull(loginFormUrl, "表单登录路径不能为空");
		this.loginFormUrl = loginFormUrl;
	}

	/**
	 * 后置验证
	 */
	@Override
	public void afterPropertiesSet() {
		Assert.isTrue(StringUtils.hasText(this.loginFormUrl) && UrlUtils.isValidRedirectUrl(this.loginFormUrl), "表单路径必须为一个重定向路径");
		Assert.isTrue(!this.useForward || !UrlUtils.isAbsoluteUrl(this.loginFormUrl), "服务器内部调用路径不能为绝对路径");
		Assert.notNull(this.portMapper, "端口映射器不能为空");
		Assert.notNull(this.portResolver, "端口解析器不能为空");
	}

	/**
	 * 获取当前请求的重定向 表单登录 URL
	 *
	 * @param request   请求对象
	 * @param response  响应对象
	 * @param exception 异常对象
	 * @return 表单登录URL
	 */
	protected String determineUrlToUseForThisRequest(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) {
		return getLoginFormUrl();
	}

	/**
	 * 执行服务器内部跳转或者重定向URL
	 *
	 * @param request       请求对象
	 * @param response      响应对象
	 * @param authException 认证异常
	 * @throws IOException      执行异常
	 * @throws ServletException 执行异常
	 */
	@Override
	public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
		// 重定向跳转
		if (!this.useForward) {
			String redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
			this.redirectStrategy.sendRedirect(request, response, redirectUrl);
			return;
		}
		// 内部服务器跳转 如果是强制https 且请求为http 尝试构建请求并重定向
		String redirectUrl = null;
		// 如果强制使用https
		if (this.forceHttps && "http".equals(request.getScheme())) {
			// 构建 服务器内部跳转路径
			redirectUrl = buildHttpsRedirectUrlForRequest(request);
		}
		// 构建成功，使用重定向策略跳转
		if (redirectUrl != null) {
			this.redirectStrategy.sendRedirect(request, response, redirectUrl);
			return;
		}
		// 服务器内部跳转
		String loginForm = determineUrlToUseForThisRequest(request, response, authException);
		logger.debug(LogMessage.format("服务器跳转路径为: %s", loginForm));
		RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);
		dispatcher.forward(request, response);
		return;
	}

	/**
	 * 构建重定向路径
	 *
	 * @param request       请求对象
	 * @param response      响应对象
	 * @param authException 认证异常
	 * @return 重定向路径
	 */
	protected String buildRedirectUrlToLoginPage(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
		// 请求路径
		String loginForm = determineUrlToUseForThisRequest(request, response, authException);
		if (UrlUtils.isAbsoluteUrl(loginForm)) {
			return loginForm;
		}
		// 请求端口
		int serverPort = this.portResolver.getServerPort(request);
		// 请求方式
		String scheme = request.getScheme();
		RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder();
		urlBuilder.setScheme(scheme);
		urlBuilder.setServerName(request.getServerName());
		urlBuilder.setPort(serverPort);
		urlBuilder.setContextPath(request.getContextPath());
		urlBuilder.setPathInfo(loginForm);
		// 如果强制使用https 且当前为http 则需要端口转换 scheme 设置
		if (this.forceHttps && "http".equals(scheme)) {
			Integer httpsPort = this.portMapper.lookupHttpsPort(serverPort);
			if (httpsPort != null) {
				urlBuilder.setScheme("https");
				urlBuilder.setPort(httpsPort);
			} else {
				logger.warn(LogMessage.format("http 端口不能转换为https端口 当前端口为 %s", serverPort));
			}
		}
		return urlBuilder.getUrl();
	}

	/**
	 * http 转https 构造服务器内部访问路径
	 *
	 * @param request 请求对象
	 * @return 访问路径
	 * @throws IOException      执行异常
	 * @throws ServletException 执行异常
	 */
	protected String buildHttpsRedirectUrlForRequest(HttpServletRequest request) throws IOException, ServletException {
		int serverPort = this.portResolver.getServerPort(request);
		Integer httpsPort = this.portMapper.lookupHttpsPort(serverPort);
		if (httpsPort != null) {
			RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder();
			urlBuilder.setScheme("https");
			urlBuilder.setServerName(request.getServerName());
			urlBuilder.setPort(httpsPort);
			urlBuilder.setContextPath(request.getContextPath());
			urlBuilder.setServletPath(request.getServletPath());
			urlBuilder.setPathInfo(request.getPathInfo());
			urlBuilder.setQuery(request.getQueryString());
			return urlBuilder.getUrl();
		}
		// 构建失败 日志记录 返回空
		logger.warn(LogMessage.format("http 端口不能转换为https端口 当前端口为 %s", serverPort));
		return null;
	}

	/**
	 * 设置是否强制使用https
	 *
	 * @param forceHttps 使用https
	 */
	public void setForceHttps(boolean forceHttps) {
		this.forceHttps = forceHttps;
	}

	/**
	 * 获取是否强制使用https配置
	 *
	 * @return 使用https
	 */
	protected boolean isForceHttps() {
		return this.forceHttps;
	}

	/**
	 * 获取表单登录路径
	 *
	 * @return 表单登录路径
	 */
	public String getLoginFormUrl() {
		return this.loginFormUrl;
	}

	/**
	 * 设置 http https 端口转换器
	 *
	 * @param portMapper HTTP https 端口转换器
	 */
	public void setPortMapper(PortMapper portMapper) {
		Assert.notNull(portMapper, "端口转换器不能为空");
		this.portMapper = portMapper;
	}

	/**
	 * 设置 http https 端口转换器
	 *
	 * @return 端口转换器
	 */
	protected PortMapper getPortMapper() {
		return this.portMapper;
	}

	/**
	 * 设置端口解析器
	 *
	 * @param portResolver 端口解析器
	 */
	public void setPortResolver(PortResolver portResolver) {
		Assert.notNull(portResolver, "端口解析器不能为空");
		this.portResolver = portResolver;
	}

	/**
	 * 获取端口解析器
	 *
	 * @return 端口解析器
	 */
	protected PortResolver getPortResolver() {
		return this.portResolver;
	}

	/**
	 * 设置是否强制使用服务器内部跳转
	 *
	 * @param useForward 设置值
	 */
	public void setUseForward(boolean useForward) {
		this.useForward = useForward;
	}

	/**
	 * 获取服务器内部跳转设置
	 *
	 * @return 设置值
	 */
	protected boolean isUseForward() {
		return this.useForward;
	}

}
